package org.truenewx.tnxjee.repo.redis.support;

import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.boot.ApplicationArguments;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.truenewx.tnxjee.core.Strings;
import org.truenewx.tnxjee.core.config.AppConstants;
import org.truenewx.tnxjee.core.util.CollectionUtil;
import org.truenewx.tnxjee.core.util.LogUtil;
import org.truenewx.tnxjee.core.util.NetUtil;
import org.truenewx.tnxjee.core.util.StringUtil;


public class RedisLockTemplate {

    // oldValue == false 不能更改为其它，否则结果不正确
    private static final String SCRIPT_WITH_EXPIRE = "local oldValue = redis.call('get', KEYS[1])\n" +
            "if oldValue == false or oldValue == ARGV[1] then\n" +
            "    redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
            "    return true\n" +
            "else return false end";
    private static final String SCRIPT_WITHOUT_EXPIRE = "local oldValue = redis.call('get', KEYS[1])\n" +
            "if oldValue == false or oldValue == ARGV[1] then\n" +
            "    redis.call('set', KEYS[1], ARGV[1])\n" +
            "    return true\n" +
            "else return false end";

    private final StringRedisTemplate redisTemplate;

    private final RedisLockKeyPrefixSupplier keyPrefixSupplier;
    /**
     * 锁的拥有者，只有锁的拥有者可以释放锁
     */
    private final String lockOwner;
    /**
     * 默认的锁过期时间（毫秒），>0时才有效，<=0时不限
     */
    private long defaultExpiredMilliseconds;
    private final Logger logger = LogUtil.getLogger(getClass());

    public RedisLockTemplate(ApplicationContext applicationContext, String keyPrefix) {
        this(applicationContext, () -> keyPrefix);
    }

    public RedisLockTemplate(ApplicationContext applicationContext, RedisLockKeyPrefixSupplier keyPrefixSupplier) {
        this.redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
        this.keyPrefixSupplier = keyPrefixSupplier;
        ApplicationArguments arguments = applicationContext.getBean(ApplicationArguments.class);
        this.lockOwner = getLockOwner(arguments);
        // 初始化时清除同源的所有可释放的锁，避免非常规退出应用后，锁长期无法释放的问题
        clear();
    }

    private static String getLockOwner(ApplicationArguments arguments) {
        // 默认以本地IP作为锁定拥有者
        String lockOwner = NetUtil.getLocalIp();
        String instance = CollectionUtil.getFirst(arguments.getOptionValues(AppConstants.ARGUMENT_INSTANCE));
        // 如果指定了实例名，则再附加上实例名
        if (StringUtils.isNotBlank(instance)) {
            lockOwner += Strings.SLASH + instance;
        }
        return lockOwner;
    }

    public void setDefaultExpiredMilliseconds(long defaultExpiredMilliseconds) {
        this.defaultExpiredMilliseconds = defaultExpiredMilliseconds;
    }

    private String getInternalKey(String key) {
        String lockName = this.keyPrefixSupplier.get();
        if (StringUtils.isNotBlank(key)) {
            lockName += key;
        }
        return lockName;
    }

    public boolean tryLock(String key, long expiredMilliseconds) {
        String script;
        Object[] args;
        if (expiredMilliseconds <= 0) {
            expiredMilliseconds = this.defaultExpiredMilliseconds;
        }
        // 锁的拥有者为空时，生成UUID作为拥有者，这意味着锁对于每次调用是互斥的
        String lockValue = getLockValue();
        if (expiredMilliseconds > 0) {
            script = SCRIPT_WITH_EXPIRE;
            args = new Object[]{lockValue, String.valueOf(expiredMilliseconds)};
        } else {
            script = SCRIPT_WITHOUT_EXPIRE;
            args = new Object[]{lockValue};
        }
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        redisScript.setResultType(Boolean.class);
        String internalKey = getInternalKey(key);
        Boolean successful = this.redisTemplate.execute(redisScript, List.of(internalKey), args);
        if (successful == null) {
            successful = Boolean.TRUE;
        }
        if (successful) {
            if (expiredMilliseconds > 0) {
                this.logger.info("The lock '{}' has been obtained, which will expire in {}ms.", internalKey,
                        expiredMilliseconds);
            } else {
                this.logger.info("The lock '{}' has been obtained.", internalKey);
            }
        } else {
            this.logger.warn("The lock '{}' obtain failed.", internalKey);
        }
        return successful;
    }

    private String getLockValue() {
        return this.lockOwner + Strings.SLASH + StringUtil.uuid32();
    }

    public boolean tryLock(String key) {
        return tryLock(key, 0);
    }

    public boolean isLocked(String key) {
        String internalKey = getInternalKey(key);
        return this.redisTemplate.opsForValue().get(internalKey) != null;
    }

    /**
     * 释放指定锁
     *
     * @param key 锁的key
     * @return 是否释放成功。不是当前应用实例锁定的锁无法释放，返回false
     */
    public boolean release(String key) {
        String internalKey = getInternalKey(key);
        return internalRelease(internalKey);
    }

    private boolean internalRelease(String internalKey) {
        String lockedValue = this.redisTemplate.opsForValue().get(internalKey);
        // 锁的拥有者（应用实例）相同即可释放锁，这意味着同一个拥有者内均可释放锁
        if (lockedValue != null && lockedValue.startsWith(this.lockOwner + Strings.SLASH)) {
            this.redisTemplate.delete(internalKey);
            this.logger.info("The lock '{}' has been released.", internalKey);
            return true;
        }
        return false;
    }

    /**
     * 清除当前应用实例所有可释放的锁
     */
    public void clear() {
        try {
            String keyPattern = this.keyPrefixSupplier.getPattern() + Strings.ASTERISK;
            Set<String> internalKeys = this.redisTemplate.keys(keyPattern);
            if (internalKeys != null) {
                for (String internalKey : internalKeys) {
                    internalRelease(internalKey);
                }
            }
        } catch (Exception e) { // 清除时不对外抛出异常
            LogUtil.error(getClass(), e);
        }
    }

}
